/*
 * Tidal - an IDE for Google Wave gadgets.
 * Copyright (C) 2010 Nathan Summers
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 * 
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.googlecode.tidal.ide;

import static com.google.wave.api.ElementType.GADGET;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.google.appengine.repackaged.com.google.common.collect.ImmutableMap;
import com.google.wave.api.AbstractRobot;
import com.google.wave.api.Blip;
import com.google.wave.api.BlipContentRefs;
import com.google.wave.api.Context;
import com.google.wave.api.ElementType;
import com.google.wave.api.FormElement;
import com.google.wave.api.Gadget;
import com.google.wave.api.Plaintext;
import com.google.wave.api.Wavelet;
import com.google.wave.api.event.DocumentChangedEvent;
import com.google.wave.api.event.FormButtonClickedEvent;
import com.google.wave.api.event.GadgetStateChangedEvent;
import com.google.wave.api.event.WaveletSelfAddedEvent;

import com.googlecode.tidal.ide.file.File;
import com.googlecode.tidal.ide.file.FileStore;
import com.googlecode.tidal.ide.file.jcache.SerializableFile;

@SuppressWarnings("serial")
public class TidalServlet extends AbstractRobot {
    private static final String CURRENT_CONTROL_SCHEME = "2";

    private static final String STATE_INSPECTOR = Config.STATIC_BASE_URL + "/gadget/state-inspector.xml";

    private static final String FILES_URL = Config.DYNAMIC_BASE_URL + "/files/";
    
    /* Controls */
    private static final String FILENAME_INPUT = "filename";
    private static final String MIME_TYPE_RADIO_BUTTON = "mime_type";
    private static final String CONTENTS_AREA = "contents";
    private static final String UPDATE_BUTTON = "update";
    private static final String ADD_FILE_BUTTON = "add_file";
    
    private static final String DEFAULT_FILE_NAME = "gadget.xml";
    
    /* data documents */
    private static final String CONTROL_SCHEME = "tidal-ide.control.scheme";
    
    private static final String DEFAULT_GADGET_XML = 
        "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n"
        + "<Module>\n"
        + "  <ModulePrefs title=\"Hello Wave!\">\n"
        + "    <Require feature=\"dynamic-height\" />\n"
        + "    <Require feature=\"wave\" />\n"
        + "  </ModulePrefs>\n"
        + "  <Content type=\"html\">\n"
        + "    <![CDATA[\n"
        + "<div id=\"hello\">Hello!</div>\n\n"
        + "<script>\n"
        + "var participants = [];\n"
        + "var state;\n\n"
        + "function participantsUpdated() {\n"
        + "  participants = wave.getParticipants();\n\n"
        + "  if (state)\n"
        + "    displayUpdate();\n"
        + "}\n\n"
        + "function stateUpdated() {\n"
        + "  state = wave.getState();\n"
        + "  if (participants)\n"
        + "    displayUpdate();\n"
        + "}\n\n"
        + "function displayUpdate() {\n"
        + "  document.getElementById('hello').innerHTML='Hello, Wave!';\n"
        + "  gadgets.window.adjustHeight();\n"
        + "}\n\n"
        + "function init() {\n"
        + "  if (wave && wave.isInWaveContainer()) {\n"
        + "    wave.setParticipantCallback(participantsUpdated);\n"
        + "    wave.setStateCallback(stateUpdated);\n"
        + "  }\n"
        + "}\n\n"
        + "gadgets.util.registerOnLoadHandler(init);\n"
        + "</script>\n"
        + "    ]]>\n"
        + "  </Content>\n"
        + "</Module>\n";
    
    static Logger log = Logger.getLogger(TidalServlet.class.getName());
    
    @Override
    protected String getRobotName() {
        return "Tidal (test version)";
    }

    @Override
    public void onWaveletSelfAdded(WaveletSelfAddedEvent event) {
        Wavelet wavelet = event.getWavelet();
        
        appendRootControls(wavelet);
        setContents(wavelet, DEFAULT_FILE_NAME, "application/xml", DEFAULT_GADGET_XML);
    }
    
    @Override
    @Capability(contexts=Context.ALL)
    public void onDocumentChanged(DocumentChangedEvent event) {
        Wavelet wavelet = event.getWavelet();
        Blip blip = event.getBlip();
        
        if (log.isLoggable(Level.FINEST))
            log.finest(String.format("Blip ID: %s, Root Blip ID: %s",
                                     blip.getBlipId(),
                                     wavelet.getRootBlipId()));
        
        upgradeControls(wavelet);
        
        if (blip.isRoot())
            updateTitle(blip);
        
        updateFile(blip);
    }
    
    @Override
    public void onFormButtonClicked(FormButtonClickedEvent event) {
        if (UPDATE_BUTTON.equals(event.getButtonName())) {
            refreshGadget(event.getBlip()); 
        } else {
            Blip newBlip = event.getWavelet().reply("\n");
            
            appendFileControls(newBlip, "", "", true);
        }
    }

    @Override
    public void onGadgetStateChanged(GadgetStateChangedEvent event) {
        Blip blip = event.getBlip();
        
        Gadget gadget = (Gadget) blip.at(event.getIndex()).value();
        
        if (gadget.getUrl().startsWith(FILES_URL)) {
            BlipContentRefs inspectorIndex = blip.first(GADGET, Gadget.restrictByUrl(STATE_INSPECTOR));
            Gadget stateInspector = (Gadget) inspectorIndex.value();
            
            Map<String, String> gadgetState = gadget.getProperties();
            Map<String, String> inspectorState = stateInspector.getProperties();

/* this is all that should be necessary, but the java libraries have a bug
            inspectorState.keySet().retainAll(gadgetState.keySet());
            inspectorState.putAll(gadgetState); */
            
/* here is the workaround */            
            Map<String, String> updates = new HashMap<String, String>(gadgetState);
            
            Set<String> removed = new HashSet<String>(inspectorState.keySet());
            removed.removeAll(gadgetState.entrySet());
            
            for (String key : removed) {
                updates.put(key, null);
            }
            
            updates.remove("url");
            
            inspectorIndex.updateElement(updates);
/* end workaround */
        }
    }
    
    private void upgradeControls(Wavelet wavelet) {
        String schemeDoc = wavelet.getDataDocuments().get(CONTROL_SCHEME);
        
        int schemeLevel = schemeDoc != null ? Integer.parseInt(schemeDoc) : 0; 
        
        if (schemeLevel == 2)
            return;
        
        if (schemeLevel < 1) {
            Blip root = wavelet.getRootBlip();
            BlipContentRefs insertPos = root.first(ElementType.BUTTON, FormElement.restrictByName(UPDATE_BUTTON));
            
            insertPos.insert(new Gadget(STATE_INSPECTOR));
            insertPos.insert("\nGadget State Inspector\n");            
        }
        
        if (schemeLevel < 2) {
            for (Blip blip : wavelet.getBlips().values()) {
//                blip.first("XML").insertAfter(new FormElement(ElementType.RADIO_BUTTON,
//                                                              "text/css",
//                                                              MIME_TYPE_RADIO_BUTTON),
//                                              new Plaintext("CSS"));
                
                blip.first("XML").insertAfter("CSS").insertAfter(new FormElement(ElementType.RADIO_BUTTON,
                                                              "text/css",
                                                              MIME_TYPE_RADIO_BUTTON));
            }
        }
        
        wavelet.getDataDocuments().set(CONTROL_SCHEME, CURRENT_CONTROL_SCHEME);
    }

    private void appendRootControls(Wavelet wavelet) {
        Blip root = wavelet.getRootBlip();

        root.append("Tidal Project\n\n");
        
        appendFileControls(root, DEFAULT_FILE_NAME, DEFAULT_GADGET_XML, false);
        
        root.append(new Gadget(FILES_URL + wavelet.getWaveId().serialise() + '/'
                                       + DEFAULT_FILE_NAME));

        
        root.append("\nGadget State Inspector\n");
        root.append(new Gadget(STATE_INSPECTOR));
        
        root.append(new FormElement(ElementType.BUTTON,
                                    UPDATE_BUTTON,
                                    "Update"));

        root.append(new FormElement(ElementType.BUTTON,
                                    ADD_FILE_BUTTON,
                                    "Add File"));

        root.appendMarkup("<br /><br /><a href=\""
                          + Config.CODE_LOCATION
                          + "\">Tidal source code</a>");
        
        wavelet.getDataDocuments().set(CONTROL_SCHEME, CURRENT_CONTROL_SCHEME);
    }

    private void appendFileControls(Blip blip, String filename, String contents, boolean appendTypeControl) {
        blip.append(new FormElement(ElementType.LABEL,
                                    "filename_l",
                                    "Filename"));

        blip.append(new FormElement(ElementType.INPUT,
                                    FILENAME_INPUT,
                                    filename));

        if (appendTypeControl) {
            blip.append(new FormElement(ElementType.RADIO_BUTTON_GROUP,
                                        MIME_TYPE_RADIO_BUTTON,
                                        "application/javascript"));

            blip.append(new FormElement(ElementType.RADIO_BUTTON,
                                        "application/javascript",
                                        MIME_TYPE_RADIO_BUTTON));
            blip.append("Javascript");

            blip.append(new FormElement(ElementType.RADIO_BUTTON,
                                        "application/xml",
                                        MIME_TYPE_RADIO_BUTTON));
            blip.append("XML");
            
            blip.append(new FormElement(ElementType.RADIO_BUTTON,
                                        "text/css",
                                        MIME_TYPE_RADIO_BUTTON));
            blip.append("CSS");

        }

        blip.append(new FormElement(ElementType.TEXTAREA,
                                    CONTENTS_AREA,
                                    contents));
    }

    private void refreshGadget(Blip blip) {
        BlipContentRefs gadgetRef = blip.first(ElementType.GADGET);
        Gadget gadget = (Gadget) gadgetRef.value();
        FormElement filename = getFilenameInput(blip);

        if (gadget != null && filename != null) {
            if (log.isLoggable(Level.FINER))
                log.finer(String.format("refresh gadget: old url: %s new url: %s",
                                        gadget.getUrl(),
                                        FILES_URL + blip.getWavelet().getWaveId().serialise() + '/'
                                        + filename.getValue()));

            gadget.setUrl(FILES_URL + blip.getWavelet().getWaveId().serialise() + '/' + filename.getValue());
            gadget.setProperty("check", "mate");
            gadgetRef.updateElement(ImmutableMap.of(Gadget.URL, FILES_URL + blip.getWavelet().getWaveId().serialise() + '/' + filename.getValue()));
        }
    }

    private void updateFile(Blip blip) {
        FormElement filename = getFilenameInput(blip);
        
        if (filename != null) {
            FormElement contents = getFormElement(blip, ElementType.TEXTAREA, CONTENTS_AREA);
            
            if (contents != null) {
                FormElement mimetype = getFormElement(blip, ElementType.RADIO_BUTTON_GROUP, MIME_TYPE_RADIO_BUTTON);
                setContents(blip.getWavelet(),
                            filename.getValue(),
                            mimetype != null ? mimetype.getValue() : "application/xml",
                            contents.getValue());
            }
        }
    }

    private FormElement getFilenameInput(Blip blip) {
        return getFormElement(blip, ElementType.INPUT, FILENAME_INPUT);
    }

    private FormElement getFormElement(Blip blip, ElementType elementType, String name) {
        return (FormElement) blip.first(elementType, FormElement.restrictByName(name)).value();
    }

    private void updateTitle(Blip blip) {
        FormElement filenameElement = getFilenameInput(blip);

        if (filenameElement != null)
            blip.getWavelet().setTitle("Tidal Project for " + filenameElement.getValue());
    }

    private void setContents(Wavelet wavelet, String filename, String mimetype, String contents) {
	String pathname = "/" + wavelet.getWaveId().serialise() + '/' + filename;
	FileStore fileStore = Config.getFileStore();

	File file = new SerializableFile();

	file.setContents(contents);
	file.setFilename(pathname);
	file.setMimeType(mimetype);

	fileStore.store(file);
    }
}